<!DOCTYPE html>
<html lang="zh-Hans">
  <head hexo-theme='https://github.com/volantis-x/hexo-theme-volantis/tree/4.3.1'>
  <meta charset="utf-8">
  <!-- SEO相关 -->
  
    
  
  <!-- 渲染优化 -->
  <meta http-equiv='x-dns-prefetch-control' content='on' />
  <link rel='dns-prefetch' href='https://cdn.jsdelivr.net'>
  <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
  <meta name="renderer" content="webkit">
  <meta name="force-rendering" content="webkit">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="HandheldFriendly" content="True" >
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  
  <link rel="preload" href="/android-note/css/first.css" as="style">
  

  <!-- 页面元数据 -->
  
  <title>不会 Android 性能优化？你只差一个开源库！ - Xander&#39;s blog</title>
  
    <meta name="keywords" content="Android,Performance">
  

  

  <!-- feed -->
  

  <!-- import meta -->
  

  <!-- link -->
  

  <!-- import link -->
  
  
  <link rel="shortcut icon" href="/android-note/images/xander_blog.ico">
  

  
    
<link rel="stylesheet" href="/android-note/css/first.css">

  

  
  <link rel="stylesheet" href="/android-note/css/style.css" media="print" onload="this.media='all';this.onload=null">
  <noscript><link rel="stylesheet" href="/android-note/css/style.css"></noscript>
  

  <script id="loadcss"></script>

  
<script>
if (/*@cc_on!@*/false || (!!window.MSInputMethodContext && !!document.documentMode))
    document.write(
	'<style>'+
		'html{'+
			'overflow-x: hidden !important;'+
			'overflow-y: hidden !important;'+
		'}'+
		'.kill-ie{'+
			'text-align:center;'+
			'height: 100%;'+
			'margin-top: 15%;'+
			'margin-bottom: 5500%;'+
		'}'+
	'</style>'+
    '<div class="kill-ie">'+
        '<h1><b>抱歉，您的浏览器无法访问本站</b></h1>'+
        '<h3>微软已经于2016年终止了对 Internet Explorer (IE) 10 及更早版本的支持，<br/>'+
        '继续使用存在极大的安全隐患，请使用当代主流的浏览器进行访问。</h3><br/>'+
        '<a target="_blank" rel="noopener" href="https://www.microsoft.com/zh-cn/WindowsForBusiness/End-of-IE-support"><strong>了解详情 ></strong></a>'+
    '</div>');
</script>


<noscript>
	<style>
		html{
			overflow-x: hidden !important;
			overflow-y: hidden !important;
		}
		.kill-noscript{
			text-align:center;
			height: 100%;
			margin-top: 15%;
			margin-bottom: 5500%;
		}
	</style>
    <div class="kill-noscript">
        <h1><b>抱歉，您的浏览器无法访问本站</b></h1>
        <h3>本页面需要浏览器支持（启用）JavaScript</h3><br/>
        <a target="_blank" rel="noopener" href="https://www.baidu.com/s?wd=启用JavaScript"><strong>了解详情 ></strong></a>
    </div>
</noscript>

</head>

  <body>
    

<header id="l_header" class="l_header auto shadow blur show" style='opacity: 0' >
  <div class='container'>
  <div id='wrapper'>
    <div class='nav-sub'>
      <p class="title"></p>
      <ul class='switcher nav-list-h m-phone' id="pjax-header-nav-list">
        <li><a id="s-comment" class="fas fa-comments fa-fw" target="_self" href='javascript:void(0)'></a></li>
        
          <li><a id="s-toc" class="s-toc fas fa-list fa-fw" target="_self" href='javascript:void(0)'></a></li>
        
      </ul>
    </div>
		<div class="nav-main">
      
        
        <a class="title flat-box" target="_self" href='/android-note/'>
          
            <img no-lazy class='logo' src='https://i.loli.net/2020/05/18/WO1oJwFsRb4gV79.png'/>
          
          
          
        </a>
      

			<div class='menu navigation'>
				<ul class='nav-list-h m-pc'>
          
          
          
            
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/
                  
                  
                  
                    id="android-note"
                  >
                  <i class='fas fa-book fa-fw'></i>BLOG
                </a>
                
              </li>
            
          
          
            
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/categories/
                  
                  
                  
                    id="android-notecategories"
                  >
                  <i class='fas fa-folder-open fa-fw'></i>分类
                </a>
                
              </li>
            
          
          
            
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/tags/
                  
                  
                  
                    id="android-notetags"
                  >
                  <i class='fas fa-tags fa-fw'></i>标签
                </a>
                
              </li>
            
          
          
            
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/archives/
                  
                  
                  
                    id="android-notearchives"
                  >
                  <i class='fas fa-archive fa-fw'></i>归档
                </a>
                
              </li>
            
          
          
            
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/about/
                  
                  
                  
                    id="android-noteabout"
                  >
                  <i class='fas fa-info-circle fa-fw'></i>关于
                </a>
                
              </li>
            
          
          
            
            
              <li>
                <a class="menuitem flat-box header toggle-mode-btn">
                  <i class='fas fa-moon fa-fw'></i>暗黑模式
                </a>
              <li>
            
          
          
				</ul>
			</div>

      <div class="m_search">
        <form name="searchform" class="form u-search-form">
          <i class="icon fas fa-search fa-fw"></i>
          <input type="text" class="input u-search-input" placeholder="Search..." />
        </form>
      </div>

			<ul class='switcher nav-list-h m-phone'>
				
					<li><a class="s-search fas fa-search fa-fw" target="_self" href='javascript:void(0)'></a></li>
				
				<li>
          <a class="s-menu fas fa-bars fa-fw" target="_self" href='javascript:void(0)'></a>
          <ul class="menu-phone list-v navigation white-box">
            
              
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/
                  
                  
                  
                    id="android-note"
                  >
                  <i class='fas fa-book fa-fw'></i>BLOG
                </a>
                
              </li>
            
          
            
              
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/categories/
                  
                  
                  
                    id="android-notecategories"
                  >
                  <i class='fas fa-folder-open fa-fw'></i>分类
                </a>
                
              </li>
            
          
            
              
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/tags/
                  
                  
                  
                    id="android-notetags"
                  >
                  <i class='fas fa-tags fa-fw'></i>标签
                </a>
                
              </li>
            
          
            
              
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/archives/
                  
                  
                  
                    id="android-notearchives"
                  >
                  <i class='fas fa-archive fa-fw'></i>归档
                </a>
                
              </li>
            
          
            
              
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/android-note/about/
                  
                  
                  
                    id="android-noteabout"
                  >
                  <i class='fas fa-info-circle fa-fw'></i>关于
                </a>
                
              </li>
            
          
            
              
            
              <li>
                <a class="menuitem flat-box header toggle-mode-btn">
                  <i class='fas fa-moon fa-fw'></i>暗黑模式
                </a>
              <li>
            
          
            
          </ul>
        </li>
			</ul>
		</div>
	</div>
  </div>
</header>

    <div id="l_body">
      <div id="l_cover">
  
    
        <div id="full" class='cover-wrapper post dock' style="display: none;">
          
            <div id='cover-backstretch'></div>
          
          <div class='cover-body'>
  <div class='top'>
    
    
      <p class="title">Xander's Blog</p>
    
    
      <p class="subtitle">A blog for Android</p>
    
  </div>
  <div class='bottom'>
    <div class='menu navigation'>
      <div class='list-h'>
        
          
            <a href="/android-note/"
              
              
              id="android-note">
              <i class='fas fa-book fa-fw'></i><p>BLOG</p>
            </a>
          
            <a href="/android-note/categories/"
              
              
              id="android-notecategories">
              <i class='fas fa-folder-open fa-fw'></i><p>分类</p>
            </a>
          
            <a href="/android-note/tags/"
              
              
              id="android-notetags">
              <i class='fas fa-tags fa-fw'></i><p>标签</p>
            </a>
          
            <a href="/android-note/archives/"
              
              
              id="android-notearchives">
              <i class='fas fa-archive fa-fw'></i><p>归档</p>
            </a>
          
            <a href="/android-note/about/"
              
              
              id="android-noteabout">
              <i class='fas fa-info-circle fa-fw'></i><p>关于</p>
            </a>
          
        
      </div>
    </div>
  </div>
</div>

          <div id="scroll-down" style="display: none;"><i class="fa fa-chevron-down scroll-down-effects"></i></div>
        </div>
    
  
  </div>

      <div id="safearea">
        <div class="body-wrapper" id="pjax-container">
          

<div class='l_main'>
  <article class="article post white-box reveal md shadow article-type-post" id="post" itemscope itemprop="blogPost">
  


  
  <div class="article-meta" id="top">
    
    
    
      <h1 class="title">
        不会 Android 性能优化？你只差一个开源库！
      </h1>
      <div class='new-meta-box'>
        
          
            
<div class='new-meta-item author'>
  <a class='author' target="_blank" href="https://github.com/XanderWang/" rel="nofollow noopener">
    <img no-lazy src="https://i.loli.net/2020/05/18/WO1oJwFsRb4gV79.png">
    <p>XanderWang</p>
  </a>
</div>

          
        
          
            
  <div class='new-meta-item category'>
    <a class='notlink'>
      <i class="fas fa-folder-open fa-fw" aria-hidden="true"></i>
      <a class="category-link" href="/android-note/categories/Android-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/">Android 性能优化</a>
    </a>
  </div>


          
        
          
            <div class="new-meta-item date">
  <a class='notlink'>
    <i class="fas fa-calendar-alt fa-fw" aria-hidden="true"></i>
    <p>发布于：Feb 24, 2021</p>
  </a>
</div>

          
        
          
            
  <div class="new-meta-item browse leancloud">
    <a class='notlink'>
      
      <div id="lc-pv" data-title="不会 Android 性能优化？你只差一个开源库！" data-path="/android-note/2021/02/24/android_performance/android_performance_lib/">
        <i class="fas fa-eye fa-fw" aria-hidden="true"></i>
        <span id='number'><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span>
        次浏览
      </div>
    </a>
  </div>


          
        
      </div>
    
  </div>


  
  
  <h1 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h1><p>由于本人工作需要，需要解决一些性能问题，虽然有 <code>Profiler</code> 、<code>Systrace </code> 等工具，但是无法实时监控，多少有些不方便，于是计划写一个能实时监控性能的小工具。经过学习大佬们的文章，最终完成了这个开源的性能实时检测库。初步能达到预期效果，这里做个记录，算是小结了。</p>
<p>开源库的<a target="_blank" rel="noopener" href="https://github.com/XanderWang/performance">地址</a>是:</p>
<blockquote>
<p><a target="_blank" rel="noopener" href="https://github.com/XanderWang/performance">https://github.com/XanderWang/performance</a></p>
</blockquote>
<p>幸苦各位能给个小小的 star 鼓励下。</p>
<p>这个性能检测库，可以检测以下问题：</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> UI 线程 block 检测。</li>
<li><input checked="" disabled="" type="checkbox"> App 的 FPS 检测。</li>
<li><input checked="" disabled="" type="checkbox"> 线程的创建和启动监控以及线程池的创建监控。</li>
<li><input checked="" disabled="" type="checkbox"> IPC (进程间通讯)监控。</li>
</ul>
<p>同时还实现了以下功能：</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> 实时通过 logcat 打印检测到的问题。</li>
<li><input checked="" disabled="" type="checkbox"> 保存检测到的信息到文件。</li>
<li><input checked="" disabled="" type="checkbox"> 提供上报信息文件接口。</li>
</ul>
<h1 id="接入指南"><a href="#接入指南" class="headerlink" title="接入指南"></a>接入指南</h1><p>1 在 <code>APP</code> 工程目录下面的 <code>build.gradle</code> 添加如下内容。</p>
<pre><code class="groovy">dependencies &#123;
  // 基础依赖，必须添加
  debugImplementation &#39;io.github.xanderwang:performance:0.3.1&#39;
  releaseImplementation &#39;io.github.xanderwang:performance-noop:0.3.1&#39;

  // hook 方案封装，必须添加
  debugImplementation &#39;io.github.xanderwang:hook:0.3.1&#39;

  // 以下是 hook 方案选择一个就好了。如果运行报错，就换另外一个，如果还是报错，就提个 issue
  // SandHook 方案，推荐添加。如果运行报错，可以替换为 epic 库。
  debugImplementation &#39;io.github.xanderwang:hook-sandhook:0.3.1&#39;

  // epic 方法。如果运行报错，可以替换为 SandHook。
  // debugImplementation &#39;io.github.xanderwang:hook-epic:0.3.1&#39;
&#125;
</code></pre>
<p>2 <code>APP</code> 工程的 <code>Application</code> 类新增类似如下初始化代码。</p>
<p>Java 初始化示例</p>
<pre><code class="java">  private void initPERF(final Context context) &#123;
    final PERF.LogFileUploader logFileUploader = new PERF.LogFileUploader() &#123;
      @Override
      public boolean upload(File logFile) &#123;
        return false;
      &#125;
    &#125;;
    PERF.init(new PERF.Builder()
        .checkUI(true, 100) // 检查 ui lock
        .checkIPC(true) // 检查 ipc 调用
        .checkFps(true, 1000) // 检查 fps
        .checkThread(true) // 检查线程和线程池
        .globalTag(&quot;test_perf&quot;) // 全局 logcat tag ,方便过滤
        .cacheDirSupplier(new PERF.IssueSupplier&lt;File&gt;() &#123;
          @Override
          public File get() &#123;
            // issue 文件保存目录
            return context.getCacheDir();
          &#125;
        &#125;)
        .maxCacheSizeSupplier(new PERF.IssueSupplier&lt;Integer&gt;() &#123;
          @Override
          public Integer get() &#123;
            // issue 文件最大占用存储空间
            return 10 * 1024 * 1024;
          &#125;
        &#125;)
        .uploaderSupplier(new PERF.IssueSupplier&lt;PERF.LogFileUploader&gt;() &#123;
          @Override
          public PERF.LogFileUploader get() &#123;
            // issue 文件上传接口
            return logFileUploader;
          &#125;
        &#125;)
        .build());
  &#125;
</code></pre>
<p>kotlin 示例</p>
<pre><code class="kotlin">  private fun doUpload(log: File): Boolean &#123;
    return false
  &#125;

  private fun initPERF(context: Context) &#123;
    PERF.init(PERF.Builder()
        .checkUI(true, 100)// 检查 ui lock
        .checkIPC(true) // 检查 ipc 调用
        .checkFps(true, 1000) // 检查 fps
        .checkThread(true)// 检查线程和线程池
        .globalTag(&quot;test_perf&quot;)// 全局 logcat tag ,方便过滤
        .cacheDirSupplier &#123; context.cacheDir &#125; // issue 文件保存目录
        .maxCacheSizeSupplier &#123; 10 * 1024 * 1024 &#125; // issue 文件最大占用存储空间
        .uploaderSupplier &#123; // issue 文件的上传接口实现
          PERF.LogFileUploader &#123; logFile -&gt; doUpload(logFile) &#125;
        &#125;
        .build()
    )
  &#125;
</code></pre>
<h1 id="主要更新记录"><a href="#主要更新记录" class="headerlink" title="主要更新记录"></a>主要更新记录</h1><ul>
<li>0.3.1  新增给 ImageView 设置比实际控件尺寸大的图片检测</li>
<li>0.3.0  修改依赖库发布方式为 MavenCentral </li>
<li>0.2.0  线程耗时的监控，同时可以监控线程优先级(setPriority)的改变。</li>
<li>0.1.12 线程创建的监控，加入 thread name 信息收集。同时接入 startup 库做必要的初始化，以及调整 multi dex 的时候，配置文件找不到的问题。</li>
<li>0.1.11 优化 hook 方案的封装，通过 SandHook 开源库，可以按照 IPC 的耗时时间长短来检测。</li>
<li>0.1.10 FPS 的检测时间间隔从默认 2s 调整为 1s，同时支持自定义时间间隔。</li>
<li>0.1.9  优化线程池创建的监控。</li>
<li>0.1.8  初版发布，完成基本的功能。</li>
</ul>
<p>不建议直接在线上使用这个库，在编写这个库，测试 hook 的时候，在不同的机器和 <code>rom</code> 上，会有不同的问题，这里建议先只在线下自测使用这个检测库。</p>
<h1 id="原理介绍"><a href="#原理介绍" class="headerlink" title="原理介绍"></a>原理介绍</h1><h2 id="UI-线程-block-检测原理"><a href="#UI-线程-block-检测原理" class="headerlink" title="UI 线程 block 检测原理"></a>UI 线程 block 检测原理</h2><p>主要参考了 <code>AndroidPerformanceMonitor</code> 库的思路，对 <code>UI</code> 线程的 <code>Looper</code> 里面处理 <code>Message</code> 的过程进行监控。</p>
<p>具体做法是，在 <code>Looper</code> 开始处理 <code>Message</code> 前，在异步线程开启一个延时任务，用于后续收集信息。如果这个 <code>Message</code> 在指定的时间段内完成了处理，那么在这个 <code>Message</code> 被处理完后，就取消之前的延时任务，说明 <code>UI</code> 线程没有 block 。如果在指定的时间段内没有完成任务，说明 <code>UI</code> 线程有 block 。此时，异步线程可以执行刚才的延时任务。如果我们在这个延时任务里面打印 <code>UI</code> 线程的方法调用栈，就可以知道 <code>UI</code> 线程在做什么了。这个就是 <code>UI</code>  线程 block 检测的基本原理。</p>
<p>但是这个方案有一个缺点，就是无法处理 <code>InputManager</code> 的输入事件，比如 <code>TV</code> 端的遥控按键事件。通过对按键事件的调用方法链进行分析，发现最终每个按键事件都调用了 <code>DecorView</code> 类的 <code>dispatchKeyEvent</code> 方法，而非 <code>Looper</code> 的处理 <code>Message</code> 流程。所以 <code>AndroidPerformanceMonitor</code> 库是无法准确监控 TV 端应用 <code>UI</code>  block 的情况。针对 <code>TV</code> 端应用按键处理，需要找到一个新的切入点，这个切入点就是刚刚的 <code>DecorView</code> 类的 <code>dispatchKeyEvent</code> 方法。</p>
<p>那如何介入 <code>DecorView</code> 类的 <code>dispatchKeyEvent</code> 方法呢？我们可以通过 <code>epic</code> 库来 <code>hook</code> 这个方法的调用。<code>hook</code> 成功后，我们可以在 <code>DecorView</code> 类的 <code>dispatchKeyEvent</code> 方法调用前后都接收到一个回调方法，在 <code>dispatchKeyEvent</code> 方法调用前我们可以在异步线程执行一个延时任务，在 <code>dispatchKeyEvent</code> 方法调用后，取消这个延时任务。如果 <code>dispatchKeyEvent</code> 方法耗时时间小于指定的时间阈值，延时任务在执行前被取消，可以认为没有 block ，此时移除了延时任务。如果 <code>dispatchKeyEvent</code> 方法耗时时间大于指定的时间阈值说明此时 <code>UI</code>  线程是有 block 的。此时，异步线程可以执行这个延时任务来收集必要的信息。</p>
<p>以上就是修改后的 <code>UI</code> 线程 block 的检测原理了，目前做的还比较粗糙，后续计划考虑参考 <code>AndroidPerformanceMonitor</code> 打印 CPU 、内存等更多的信息。</p>
<p>最终终端 log 打印效果如下：</p>
<pre><code>com.xander.performace.demo W/demo_Issue: =================================================
    type: UI BLOCK
    msg: UI BLOCK
    create time: 2021-01-13 11:24:41
    trace:
        java.lang.Thread.sleep(Thread.java:-2)
        java.lang.Thread.sleep(Thread.java:442)
        java.lang.Thread.sleep(Thread.java:358)
        com.xander.performance.demo.MainActivity.testANR(MainActivity.kt:49)
        java.lang.reflect.Method.invoke(Method.java:-2)
        androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397)
        android.view.View.performClick(View.java:7496)
        android.view.View.performClickInternal(View.java:7473)
        android.view.View.access$3600(View.java:831)
        android.view.View$PerformClick.run(View.java:28641)
        android.os.Handler.handleCallback(Handler.java:938)
        android.os.Handler.dispatchMessage(Handler.java:99)
        android.os.Looper.loop(Looper.java:236)
        android.app.ActivityThread.main(ActivityThread.java:7876)
        java.lang.reflect.Method.invoke(Method.java:-2)
        com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)
</code></pre>
<h2 id="FPS-检测的原理"><a href="#FPS-检测的原理" class="headerlink" title="FPS 检测的原理"></a>FPS 检测的原理</h2><p>FPS 检测的原理，利用了 Android 的屏幕绘制原理。这里简单说下 Android 的屏幕绘制原理。</p>
<p>系统每隔 16 ms 就会发送一个 <code>VSync</code> 信号。 如果应用注册了这个 <code>VSync</code> 信号，就会在 <code>VSync </code> 信号到来的时候，收到回调，从而开始准备绘制。如果准备顺利，也就是 <code>CPU</code> 准备数据、<code>GPU</code> 栅格化等，如果这些任务在 16 ms 之内完成，那么下一个 <code>VSync</code> 信号到来前就可以绘制这一帧界面了。就没有掉帧，界面很流畅。如果在 16 ms 内没准备好，可能就需要更多的时间这个画面才能显示出来，在这种情况下就发生了丢帧，如果丢帧很多就卡顿了。</p>
<p>检测 FPS 的原理其实挺简单的，就是通过一段时间内，比如 1s，统计绘制了多少个画面，就可以计算出 FPS 了。那如何知道应用 1s 内绘制了多少个界面呢？这个就要靠 <code>VSync</code> 信号监听了。</p>
<p>在开始准备绘制前，往 <code>UI</code> 线程的 <code>MessageQueue</code> 里面放一个<strong>同步屏障</strong>，这样 <code>UI</code> 线程就只会处理异步消息，直到<strong>同步屏障</strong>被移除。刷新前，应用会注册一个 <code>VSync</code> 信号监听，当 <code>VSync</code> 信号到达的时候，系统会通知应用，让应用会给 <code>UI</code> 线程的 <code>MessageQueue</code> 里面放一个<strong>异步 <code>Message</code> <strong>。由于之前 <code>MessageQueue</code> 里有了一个</strong>同步屏障</strong>，所以后续 <code>UI</code> 线程会优先处理这个<strong>异步 <code>Message</code></strong> 。这个<strong>异步 <code>Message</code></strong> 做的事情就是从  <code>ViewRootImpl</code> 开始我们熟悉的 <code>measure</code> 、<code>layout</code> 和 <code>draw</code> 。</p>
<p>我们可以通过 <code>Choreographer</code> 注册 <code>VSync</code> 信号监听。16ms 后，我们收到了 <code>VSync</code> 的信号，给 <code>MessageQueue</code> 里面放一个<strong>同步消息</strong>，我们不做特别处理，只是做一个计数，然后监听下一次的 <code>VSync</code> 信号，这样，我们就可以知道 1s 内我们监听到了多少个 <code>VSync</code> 信号，就可以得出帧率。</p>
<p>为什么监听到的 <code>VSync</code> 信号数量就是帧率呢？</p>
<p>由于 <code>Looper</code> 处理 <code>Message</code> 是串行的，就是一次只处理一个 <code>Message</code> ，处理完了这个 <code>Message</code> 才会处理下一个 <code>Message</code> 。而绘制的时候，绘制任务 <code>Message</code> 是异步消息，会优先执行，绘制任务 <code>Message</code> 执行完成后，就会执行上面说的 <code>VSync</code> 信号计数的任务。如果忽略计数任务的耗时，那么最后统计到的 <code>VSync</code> 信号数量可以粗略认为是某段时间内绘制的帧数。然后就可以通过这段时间的长度和 <code>VSync</code> 信号数量来计算帧率了。</p>
<p>最终终端 log 打印效果如下：</p>
<pre><code>com.xander.performace.demo W/demo_FPSTool: APP FPS is: 54 Hz
com.xander.performace.demo W/demo_FPSTool: APP FPS is: 60 Hz
com.xander.performace.demo W/demo_FPSTool: APP FPS is: 60 Hz
</code></pre>
<h2 id="线程的创建和启动监控以及线程池的创建监控"><a href="#线程的创建和启动监控以及线程池的创建监控" class="headerlink" title="线程的创建和启动监控以及线程池的创建监控"></a>线程的创建和启动监控以及线程池的创建监控</h2><p>线程和线程池的监控，主要是监控线程和线程池在哪里创建和执行的，如果我们可以知道这些信息，我们就可以比较清楚线程和线程池的创建和启动时机是否合理。从而得出优化方案。</p>
<p>一个比较容易想到的方法就是，应用代码里面的所有线程和线程池继承同一个线程基类和线程池基类。然后在构造函数和启动函数里面打印方法调用栈，这样我们就知道哪里创建和执行了线程或者线程池。</p>
<p>让应用所有的线程和线程池继承同一个基类，可以通过编译插件来实现，定制一个特殊的 <code>Transform</code> ，通过 <code>ASM</code> 编辑生成的字节码来改变继承关系。但是，这个方法有一定的上手难度，不太适合新手。</p>
<p>除了这个方法，我们还有另外一种方法，就是 <code>hook</code> 。通过 <code>hook</code> 线程或者线程池的构造方法和启动方法，我们就可以在线程或者线程池的构造方法和启动方法的前后做一些切片处理，比如打印当前方法调用栈等。这个也就是线程和线程池监控的基本原理。</p>
<p>线程池的监控没有太大难度，一般都是 <code>ThreadPoolExecutor</code> 的子类，所以我们 <code>hook</code> 一下 <code>ThreadPoolExecutor</code> 的构造方法就可以监控线程池的创建了。线程池的执行主要就是 <code>hook</code> 住 <code>ThreadPoolExecutor</code> 类的 <code>execute</code> 方法。</p>
<p>线程的创建和执行的监控方法就稍微要费些脑筋了，因为线程池里面会创建线程，所以这个线程的创建和执行应该和线程池绑定的。需要找到线程和线程池的联系，之前看到一个库，好像是通过线程和线程池的 <code>ThreadGroup</code> 来建立关联的，本来我也计划按照这个关系来写代码的，但是我发现，我们有的小伙伴写的线程池的 <code>ThreadFactory</code> 里面创建线程并没有传入<code>ThreadGroup</code> ，这个就尴尬了，就建立不了联系了。经过查阅相关源码发现了一个关键的类，<code>ThreadPoolExecutor</code> 的内部类<code>Worker</code> ，由于这个类是内部类，所以这个类实际的构造方法里面会传入一个外部类的实例，也就是 <code>ThreadPoolExecutor</code> 实例。同时， <code>Worker</code> 这个类还是一个 <code>Runnable</code> 实现，在 <code>Worker</code> 类通过 <code>ThreadFactory</code> 创建线程的时候，会把自己作为一个 <code>Runnable</code> 传给 <code>Thread</code> 所以，我们通过这个关系，就可以知道 <code>Worker</code> 和 <code>Thread</code> 的关联了。这样，我们通过 <code>ThreadPoolExecutor</code> 和 <code>Worker</code> 的关联，以及 <code>Worker</code> 和 <code>Thread</code> 的关联，就可以得到 <code>ThreadPoolExecutor</code> 和它创建的 <code>Thread</code> 的关联了。这个也就是线程和线程池的监控原理了。</p>
<p>最终终端 log 打印效果如下：</p>
<pre><code>com.xander.performace.demo W/demo_Issue: =================================================
    type: THREAD
    msg: THREAD POOL CREATE
    create time: 2021-01-13 11:23:47
    create trace:
        com.xander.performance.StackTraceUtils.list(StackTraceUtils.java:39)
        com.xander.performance.ThreadTool$ThreadPoolExecutorConstructorHook.afterHookedMethod(ThreadTool.java:158)
        de.robv.android.xposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:265)
        me.weishu.epic.art.entry.Entry64.onHookObject(Entry64.java:64)
        me.weishu.epic.art.entry.Entry64.referenceBridge(Entry64.java:239)
        java.util.concurrent.Executors.newSingleThreadExecutor(Executors.java:179)
        com.xander.performance.demo.MainActivity.testThreadPool(MainActivity.kt:38)
        java.lang.reflect.Method.invoke(Method.java:-2)
        androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397)
        android.view.View.performClick(View.java:7496)
        android.view.View.performClickInternal(View.java:7473)
        android.view.View.access$3600(View.java:831)
        android.view.View$PerformClick.run(View.java:28641)
        android.os.Handler.handleCallback(Handler.java:938)
        android.os.Handler.dispatchMessage(Handler.java:99)
        android.os.Looper.loop(Looper.java:236)
        android.app.ActivityThread.main(ActivityThread.java:7876)
        java.lang.reflect.Method.invoke(Method.java:-2)
        com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)
</code></pre>
<h2 id="IPC-进程间通讯-监控的原理"><a href="#IPC-进程间通讯-监控的原理" class="headerlink" title="IPC(进程间通讯)监控的原理"></a>IPC(进程间通讯)监控的原理</h2><p>进程间通讯的具体原理，也就是 <code>Binder</code> 机制，这里不做详细的说明，也不是这个框架库的原理。</p>
<p>检测进程间通讯的方法和前面检测线程的方法类似，就是找到所有的进程间通讯的方法的共同点，然后对共同点做一些修改或者说切片，让应用在进行进程间通讯的时候，打印一下调用栈，然后继续做原来的事情。就达到了 IPC 监控的目的。</p>
<p>那如何找到共同点，或者说切片，就是本节的重点。</p>
<p>进程间通讯离不开 <code>Binder</code> ，需要从 <code>Binder</code> 入手。</p>
<p>写一个 <code>AIDL</code> demo 后发现，自动生成的代码里面，接口 <code>A</code> 继承自 <code>IInterface</code> 接口，然后接口里面有个内部抽象类 <code>Stub</code> 类，继承自 <code>Binder</code> ，同时实现了接口 <code>A</code> 。这个 <code>Stub</code> 类里面还有一个内部类 <code>Proxy</code> ，实现了接口 <code>A</code> ，并持有一个 <code>IBinder</code> 实例。</p>
<p>我们在使用 <code>AIDL</code> 的时候，会用到 <code>Stub</code> 类的 <code>asInterFace</code> 的方法，这个方法会新建一个 <code>Proxy</code> 实例，并给这个 <code>Proxy</code> 实例传入 <code>IBinder</code> , 或者如果传入的 <code>IBinder</code> 实例如果是接口 <code>A</code> 的话，就强制转化为接口 A 实例。一般而言，这个 <code>IBinder</code> 实例是 <code>ServiceConnection</code> 的回调方法里面的实例，是 <code>BinderProxy</code> 的实例。所以 <code>Stub</code> 类的 <code>asInterFace</code> 一般会创建一个 <code>Proxy</code> 实例，查看这个 <code>Proxy</code> 接口的实现方法，发现最终都会调用 <code>BinderProxy</code> 的 <code>transact</code> 方法，所以 <code>BinderProxy</code> 的 <code>transact</code> 方法是一个很好的切入点。</p>
<p>本来我也是计划通过 <code>hook</code> 住 <code>BinderProxy</code> 类的 <code>transact</code> 方法来做 IPC 的检测的。但是 <code>epic</code> 库在 <code>hook</code> 含有 <code>Parcel</code> 类型参数的方法的时候，不稳定，会有异常。由于暂时还没能力解决这个异常，只能重新找切入点。最后发现 <code>AIDL</code> demo 生成的代码里面，除了调用了 调用 <code>BinderProxy</code> 的 <code>transact</code> 方法外，还调用了 <code>Parcel</code> 的 <code>readException</code> 方法，于是决定 <code>hook</code> 这个方法来切入 <code>IPC</code> 调用流程，从而达到 <code>IPC</code> 监控的目的。</p>
<p>最终终端 log 打印效果如下：</p>
<pre><code>com.xander.performace.demo W/demo_Issue: =================================================
    type: IPC
    msg: IPC
    create time: 2021-01-13 11:25:04
    trace:
        com.xander.performance.StackTraceUtils.list(StackTraceUtils.java:39)
        com.xander.performance.IPCTool$ParcelReadExceptionHook.beforeHookedMethod(IPCTool.java:96)
        de.robv.android.xposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:229)
        me.weishu.epic.art.entry.Entry64.onHookVoid(Entry64.java:68)
        me.weishu.epic.art.entry.Entry64.referenceBridge(Entry64.java:220)
        me.weishu.epic.art.entry.Entry64.voidBridge(Entry64.java:82)
        android.app.IActivityManager$Stub$Proxy.getRunningAppProcesses(IActivityManager.java:7285)
        android.app.ActivityManager.getRunningAppProcesses(ActivityManager.java:3684)
        com.xander.performance.demo.MainActivity.testIPC(MainActivity.kt:55)
        java.lang.reflect.Method.invoke(Method.java:-2)
        androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397)
        android.view.View.performClick(View.java:7496)
        android.view.View.performClickInternal(View.java:7473)
        android.view.View.access$3600(View.java:831)
        android.view.View$PerformClick.run(View.java:28641)
        android.os.Handler.handleCallback(Handler.java:938)
        android.os.Handler.dispatchMessage(Handler.java:99)
        android.os.Looper.loop(Looper.java:236)
        android.app.ActivityThread.main(ActivityThread.java:7876)
        java.lang.reflect.Method.invoke(Method.java:-2)
        com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)
</code></pre>
<h1 id="联系我"><a href="#联系我" class="headerlink" title="联系我"></a>联系我</h1><ul>
<li> Mail</li>
</ul>
<p><a href="mailto:&#52;&#x32;&#48;&#54;&#x34;&#x30;&#x37;&#x36;&#51;&#64;&#113;&#x71;&#46;&#99;&#x6f;&#109;">&#52;&#x32;&#48;&#54;&#x34;&#x30;&#x37;&#x36;&#51;&#64;&#113;&#x71;&#46;&#99;&#x6f;&#109;</a></p>
<ul>
<li>微信</li>
</ul>
<p><img src="https://s3.ax1x.com/2021/01/30/yASVMD.jpg" class="lazyload placeholder" data-srcset="https://s3.ax1x.com/2021/01/30/yASVMD.jpg" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg" alt="微信"></p>
<h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料:"></a>参考资料:</h1><ol>
<li><a target="_blank" rel="noopener" href="https://github.com/tiann/epic">epic</a></li>
<li><a target="_blank" rel="noopener" href="https://github.com/ganyao114/SandHook">SandHook</a></li>
<li><a target="_blank" rel="noopener" href="https://github.com/markzhai/AndroidPerformanceMonitor">AndroidPerformanceMonitor</a></li>
<li><a target="_blank" rel="noopener" href="https://juejin.cn/post/6890407553457963022">面试官：如何监测应用的 FPS ？</a></li>
<li><a target="_blank" rel="noopener" href="https://juejin.cn/post/6844904066259091469">深入探索Android卡顿优化（下）</a></li>
</ol>

  
  
    
    <div class='footer'>
      
      
      
        <div class='copyright'>
          <blockquote>
            
              
                <p>本文永久链接是：<a href=http://xander_wang.gitee.io/android-note/2021/02/24/android_performance/android_performance_lib/>http://xander_wang.gitee.io/android-note/2021/02/24/android_performance/android_performance_lib/</a></p>
              
            
          </blockquote>
        </div>
      
      
    </div>
  
  
    


  <div class='article-meta' id="bottom">
    <div class='new-meta-box'>
      
        
          
<div class='new-meta-item author'>
  <a class='author' target="_blank" href="https://github.com/XanderWang/" rel="nofollow noopener">
    <img no-lazy src="https://i.loli.net/2020/05/18/WO1oJwFsRb4gV79.png">
    <p>XanderWang</p>
  </a>
</div>

        
      
        
          <div class="new-meta-item date" itemprop="dateUpdated" datetime="2022-12-11T10:26:22+00:00">
  <a class='notlink'>
    <i class="fas fa-edit fa-fw" aria-hidden="true"></i>
    <p>更新于：Dec 11, 2022</p>
  </a>
</div>

        
      
        
          
  
  <div class="new-meta-item meta-tags"><a class="tag" href="/android-note/tags/Android/" rel="nofollow"><i class="fas fa-hashtag fa-fw" aria-hidden="true"></i><p>Android</p></a></div> <div class="new-meta-item meta-tags"><a class="tag" href="/android-note/tags/Performance/" rel="nofollow"><i class="fas fa-hashtag fa-fw" aria-hidden="true"></i><p>Performance</p></a></div>


        
      
        
          
  <div class="new-meta-item share -mob-share-list">
  <div class="-mob-share-list share-body">
    
      
        <a class="-mob-share-qq" title="" rel="external nofollow noopener noreferrer noopener"
          
          target="_blank" href="http://connect.qq.com/widget/shareqq/index.html?url=http://xander_wang.gitee.io/android-note/2021/02/24/android_performance/android_performance_lib/&title=不会 Android 性能优化？你只差一个开源库！ - Xander's blog&summary="
          
          >
          
            <img src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/qq.png" class="lazyload placeholder" data-srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/qq.png" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg">
          
        </a>
      
    
      
        <a class="-mob-share-qzone" title="" rel="external nofollow noopener noreferrer noopener"
          
          target="_blank" href="https://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=http://xander_wang.gitee.io/android-note/2021/02/24/android_performance/android_performance_lib/&title=不会 Android 性能优化？你只差一个开源库！ - Xander's blog&summary="
          
          >
          
            <img src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/qzone.png" class="lazyload placeholder" data-srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/qzone.png" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg">
          
        </a>
      
    
      
        <a class="-mob-share-weibo" title="" rel="external nofollow noopener noreferrer noopener"
          
          target="_blank" href="http://service.weibo.com/share/share.php?url=http://xander_wang.gitee.io/android-note/2021/02/24/android_performance/android_performance_lib/&title=不会 Android 性能优化？你只差一个开源库！ - Xander's blog&summary="
          
          >
          
            <img src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/weibo.png" class="lazyload placeholder" data-srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/weibo.png" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg">
          
        </a>
      
    
      
        
        <div class='hoverbox'>
          <a class='share'><img src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/wechat.png" class="lazyload placeholder" data-srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/wechat.png" srcset="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg"></a>
          <div class='target'>
            <img src="">
          </div>
        </div>
      
    
      
    
  </div>
</div>



        
      
    </div>
  </div>


  
  

  
    <div class="prev-next">
      
        <a class='prev' href='/android-note/2021/02/25/blog/hexo_create_blog/'>
          <p class='title'><i class="fas fa-chevron-left" aria-hidden="true"></i>利用 GitHub 搭建自己的个人博客</p>
          <p class='content'>先看下预览图吧。

先说下方案:

 GitHub Page + GitHub Action + hexo &amp; 配套主题 

GitHub Page这个都不陌生吧，注册 GitHub 账...</p>
        </a>
      
      
        <a class='next' href='/android-note/2021/02/07/android_note/android_note_03/'>
          <p class='title'>Android 开发总结笔记(三) - Java 集合总结<i class="fas fa-chevron-right" aria-hidden="true"></i></p>
          <p class='content'>Java 集合总结Java 有哪些集合，继承关系是怎么样的
平时常用的集合有哪些
Collection
List 可以重复添加元素
ArrayList
LinkedList


Set 不能重复...</p>
        </a>
      
    </div>
  
</article>


  

  <article class="post white-box reveal shadow" id="comments">
    <p ct><i class='fas fa-comments'></i> 评论</p>
    
    <div id="valine_container" class="valine_thread">
  <i class="fas fa-cog fa-spin fa-fw fa-2x"></i>
</div>

  </article>






</div>
<aside class='l_side'>
  
  
    
    



  <section class="widget toc-wrapper shadow desktop mobile" id="toc-div" >
    
  <header>
    
      <i class="fas fa-list fa-fw" aria-hidden="true"></i><span class='name'>本文目录</span>
    
  </header>


    <div class='content'>
        <ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#UI-%E7%BA%BF%E7%A8%8B-block-%E6%A3%80%E6%B5%8B%E5%8E%9F%E7%90%86"><span class="toc-text">UI 线程 block 检测原理</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#FPS-%E6%A3%80%E6%B5%8B%E7%9A%84%E5%8E%9F%E7%90%86"><span class="toc-text">FPS 检测的原理</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%88%9B%E5%BB%BA%E5%92%8C%E5%90%AF%E5%8A%A8%E7%9B%91%E6%8E%A7%E4%BB%A5%E5%8F%8A%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E5%88%9B%E5%BB%BA%E7%9B%91%E6%8E%A7"><span class="toc-text">线程的创建和启动监控以及线程池的创建监控</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#IPC-%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E8%AE%AF-%E7%9B%91%E6%8E%A7%E7%9A%84%E5%8E%9F%E7%90%86"><span class="toc-text">IPC(进程间通讯)监控的原理</span></a></li></ol>
    </div>
  </section>


  


</aside>



		  
		  <!--此文件用来存放一些不方便取值的变量-->
<!--思路大概是将值藏到重加载的区域内-->

<script>
  window.pdata={}
  pdata.ispage=true;
  pdata.postTitle="不会 Android 性能优化？你只差一个开源库！";
  pdata.commentPath="";
  pdata.commentPlaceholder="";
  // header 这里无论是否开启pjax都需要
  var l_header=document.getElementById("l_header");
  
  l_header.classList.add("show");
  
  
    // cover
    var cover_wrapper=document.querySelector('.cover-wrapper');
    
    cover_wrapper.id="none";
    cover_wrapper.style.display="none";
    
  
</script>

        </div>
        
  
  <footer class="footer clearfix">
    <br><br>
    
      
        <div><p>Blog content follows the <a target="_blank" rel="noopener" href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en">Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) License</a></p>
</div>
      
    
      
        
          <div><p><span id="lc-sv">本站总访问量为 <span id='number'><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span> 次</span> <span id="lc-uv">访客数为 <span id='number'><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span> 人</span></p>
</div>
        
      
    
      
        Use
        <a href="https://github.com/volantis-x/hexo-theme-volantis/tree/4.3.1" target="_blank" class="codename">Volantis</a>
        as theme, total visits
          <span id="busuanzi_value_site_pv"><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span>
          times
        
      
    
      
        <div class='copyright'>
        <p><a href="/">Copyright © 2017-2020 XXX</a></p>

        </div>
      
    
      
        
      
    
  </footer>


        <a id="s-top" class="fas fa-arrow-up fa-fw" href="javascript:void(0)"></a>
      </div>
    </div>
    <div>
      <script>
/************这个文件存放不需要重载的全局变量和全局函数*********/
window.volantis={};
window.volantis.loadcss=document.getElementById("loadcss");
/******************** Pjax ********************************/
function VPjax(){
	this.list=[] // 存放回调函数
	this.start=()=>{
	  for(var i=0;i<this.list.length;i++){
		this.list[i].run();
	  }
	}
	this.push=(fn,name)=>{
		var f=new PjaxItem(fn,name);
		this.list.push(f);
	}
	// 构造一个可以run的对象
	function PjaxItem(fn,name){
		// 函数名称
		this.name = name || fn.name
		// run方法
		this.run=()=>{
			fn()
		}
	}
}
volantis.pjax={}
volantis.pjax.method={
	complete: new VPjax(),
	error: new VPjax(),
	send: new VPjax()
}
volantis.pjax={
	...volantis.pjax,
	push: volantis.pjax.method.complete.push,
	error: volantis.pjax.method.error.push,
	send: volantis.pjax.method.send.push
}
/********************脚本懒加载函数********************************/
// 已经加入了setTimeout
function loadScript(src, cb) {
	setTimeout(function() {
		var HEAD = document.getElementsByTagName('head')[0] || document.documentElement;
		var script = document.createElement('script');
		script.setAttribute('type','text/javascript');
		if (cb) script.onload = cb;
		script.setAttribute('src', src);
		HEAD.appendChild(script);
	});
}
//https://github.com/filamentgroup/loadCSS
var loadCSS = function( href, before, media, attributes ){
	var doc = window.document;
	var ss = doc.createElement( "link" );
	var ref;
	if( before ){
		ref = before;
	}
	else {
		var refs = ( doc.body || doc.getElementsByTagName( "head" )[ 0 ] ).childNodes;
		ref = refs[ refs.length - 1];
	}
	var sheets = doc.styleSheets;
	if( attributes ){
		for( var attributeName in attributes ){
			if( attributes.hasOwnProperty( attributeName ) ){
				ss.setAttribute( attributeName, attributes[attributeName] );
			}
		}
	}
	ss.rel = "stylesheet";
	ss.href = href;
	ss.media = "only x";
	function ready( cb ){
		if( doc.body ){
			return cb();
		}
		setTimeout(function(){
			ready( cb );
		});
	}
	ready( function(){
		ref.parentNode.insertBefore( ss, ( before ? ref : ref.nextSibling ) );
	});
	var onloadcssdefined = function( cb ){
		var resolvedHref = ss.href;
		var i = sheets.length;
		while( i-- ){
			if( sheets[ i ].href === resolvedHref ){
				return cb();
			}
		}
		setTimeout(function() {
			onloadcssdefined( cb );
		});
	};
	function loadCB(){
		if( ss.addEventListener ){
			ss.removeEventListener( "load", loadCB );
		}
		ss.media = media || "all";
	}
	if( ss.addEventListener ){
		ss.addEventListener( "load", loadCB);
	}
	ss.onloadcssdefined = onloadcssdefined;
	onloadcssdefined( loadCB );
	return ss;
};
</script>
<script>
  
  loadCSS("https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.14/css/all.min.css", window.volantis.loadcss);
  
  
  loadCSS("https://cdn.jsdelivr.net/gh/l-lin/font-awesome-animation/dist/font-awesome-animation.min.css", window.volantis.loadcss);
  
  
  loadCSS("https://cdn.jsdelivr.net/npm/node-waves@0.7.6/dist/waves.min.css", window.volantis.loadcss);
  
  
</script>
<!-- required -->

<script src="https://cdn.jsdelivr.net/npm/jquery@3.5/dist/jquery.min.js"></script>

<script>
  function pjax_fancybox() {
    $(".md .gallery").find("img").each(function () { //渲染 fancybox
      var element = document.createElement("a"); // a 标签
      $(element).attr("class", "fancybox");
      $(element).attr("pjax-fancybox", "");  // 过滤 pjax
      $(element).attr("href", $(this).attr("src"));
      if ($(this).attr("data-original")) {
        $(element).attr("href", $(this).attr("data-original"));
      }
      $(element).attr("data-fancybox", "images");
      var caption = "";   // 描述信息
      if ($(this).attr('alt')) {  // 判断当前页面是否存在描述信息
        $(element).attr('data-caption', $(this).attr('alt'));
        caption = $(this).attr('alt');
      }
      var div = document.createElement("div");
      $(div).addClass("fancybox");
      $(this).wrap(div); // 最外层套 div ，其实主要作用还是 class 样式
      var span = document.createElement("span");
      $(span).addClass("image-caption");
      $(span).text(caption); // 加描述
      $(this).after(span);  // 再套一层描述
      $(this).wrap(element);  // 最后套 a 标签
    })
    $(".md .gallery").find("img").fancybox({
      selector: '[data-fancybox="images"]',
      hash: false,
      loop: false,
      closeClick: true,
      helpers: {
        overlay: {closeClick: true}
      },
      buttons: [
        "zoom",
        "close"
      ]
    });
  };
  function SCload_fancybox() {
    if ($(".md .gallery").find("img").length == 0) return;
    loadCSS("https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css", document.getElementById("loadcss"));
    loadScript('https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.js', pjax_fancybox)
  };
  $(function () {
    SCload_fancybox();
  });
  function Pjax_SCload_fancybox(){
	if (typeof $.fancybox == "undefined") {
	 SCload_fancybox();
    } else {
	 pjax_fancybox();
    }
  }
  volantis.pjax.push(Pjax_SCload_fancybox)
  volantis.pjax.send(()=>{
      if (typeof $.fancybox != "undefined") {
        $.fancybox.close();    // 关闭弹窗
      }
  },'fancybox')
</script>


<!-- internal -->

  
  
  
    
<script src="https://cdn.jsdelivr.net/npm/jquery-backstretch@2.1.18/jquery.backstretch.min.js"></script>

    <script type="text/javascript">
        var imgs=["https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/001.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/002.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/003.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/004.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/005.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/006.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/012.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/016.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/019.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/033.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/034.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/035.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/038.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/039.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/042.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/046.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/051.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/052.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/054.jpg", "https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/056.jpg"];
        if ('true' == 'true') {
          function shuffle(arr){
            /*From countercurrent-time*/
            var n = arr.length;
            while(n--) {
              var index = Math.floor(Math.random() * n);
              var temp = arr[index];
              arr[index] = arr[n];
              arr[n] = temp;
            }
          }
          shuffle(imgs);
        }
	  function Pjax_backstretch(){
        
          $('#cover-backstretch').backstretch(
            imgs,
          {
            duration: "10000",
            fade: "1500"
          });
        
	  }
	  loadScript("https://cdn.jsdelivr.net/npm/jquery-backstretch@2.1.18/jquery.backstretch.min.js",Pjax_backstretch)
    </script>
  




<script>
  function loadIssuesJS() {
    if ($(".md").find(".issues-api").length == 0) return;
	
	  loadScript('/android-note/js/issues.js');
	
  };
  $(function () {
    loadIssuesJS();
  });
  volantis.pjax.push(()=>{
	if (typeof IssuesAPI == "undefined") {
	  loadIssuesJS();
	}
  },"IssuesJS")
</script>



  <script defer src="https://cdn.jsdelivr.net/npm/vanilla-lazyload@17.1.0/dist/lazyload.min.js"></script>
<script>
  // https://www.npmjs.com/package/vanilla-lazyload
  // Set the options globally
  // to make LazyLoad self-initialize
  window.lazyLoadOptions = {
    elements_selector: ".lazyload",
    threshold: 0
  };
  // Listen to the initialization event
  // and get the instance of LazyLoad
  window.addEventListener(
    "LazyLoad::Initialized",
    function (event) {
      window.lazyLoadInstance = event.detail.instance;
    },
    false
  );
  document.addEventListener('DOMContentLoaded', function () {
    lazyLoadInstance.update();
  });
  document.addEventListener('pjax:complete', function () {
    lazyLoadInstance.update();
  });
</script>




  

<script>
  window.FPConfig = {
	delay: 0,
	ignoreKeywords: [],
	maxRPS: 5,
	hoverDelay: 25
  };
</script>
<script defer src="https://cdn.jsdelivr.net/gh/gijo-varghese/flying-pages@2.1.2/flying-pages.min.js"></script>





  <script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
    var clipboard = new ClipboardJS('.btn-copy', {
        target: function (trigger) {
            return trigger.nextElementSibling
        }
    });
    function wait(callback, seconds) {
        var timelag = null;
        timelag = window.setTimeout(callback, seconds)
    }
    function pjax_initCopyCode() {
		if($(".highlight .code pre").length+$(".article pre code").length==0)return;
        var copyHtml = '';
        copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
        copyHtml += '<i class="fas fa-copy"></i><span>COPY</span>';
        copyHtml += '</button>';
        $(".highlight .code pre").before(copyHtml);
        $(".article pre code").before(copyHtml);
        clipboard.off('success').on('success', function (e) {
            let $btn = $(e.trigger);
            $btn.addClass('copied');
            let $icon = $($btn.find('i'));
            $icon.removeClass('fa-copy');
            $icon.addClass('fa-check-circle');
            let $span = $($btn.find('span'));
            $span[0].innerText = 'COPIED';
            wait(function () {
                $icon.removeClass('fa-check-circle');
                $icon.addClass('fa-copy');
                $span[0].innerText = 'COPY'
            }, 2000)
        });
        clipboard.off('error').on('error', function (e) {
            e.clearSelection();
            let $btn = $(e.trigger);
            $btn.addClass('copy-failed');
            let $icon = $($btn.find('i'));
            $icon.removeClass('fa-copy');
            $icon.addClass('fa-times-circle');
            let $span = $($btn.find('span'));
            $span[0].innerText = 'COPY FAILED';
            wait(function () {
                $icon.removeClass('fa-times-circle');
                $icon.addClass('fa-copy');
                $span[0].innerText = 'COPY'
            }, 2000)
        })
    }
    $(function () {
        pjax_initCopyCode()
    });
	volantis.pjax.push(pjax_initCopyCode)
</script>




   <script type="text/javascript">
  loadScript("https://cdn.jsdelivr.net/npm/scrollreveal@4.0.6/dist/scrollreveal.min.js")
  function pjax_scrollrebeal() {
    ScrollReveal().reveal('.l_main .reveal', {
      distance: '32px',
      duration: '800',
      interval: '20',
      scale: '1',
      easing: 'ease-out'
    });
  }
  $(function () {
  var checkScrollReveal = setInterval(function () {
    if ($("#safearea").css("display")!="block") return
    if (typeof ScrollReveal=="undefined") return
    clearInterval(checkScrollReveal)
	pjax_scrollrebeal();
  }, 100)
  });
  volantis.pjax.push(pjax_scrollrebeal)
</script>






  
  
<script src="https://cdn.jsdelivr.net/npm/valine@1.4/dist/Valine.min.js"></script>


<script>
  function emoji(path, idx, ext) {
    return path + "/" + path + "-" + idx + "." + ext;
  }
  var emojiMaps = {};
  for (var i = 1; i <= 54; i++) {
    emojiMaps['tieba-' + i] = emoji('tieba', i, 'png');
  }
  for (var i = 1; i <= 101; i++) {
    emojiMaps['qq-' + i] = emoji('qq', i, 'gif');
  }
  for (var i = 1; i <= 116; i++) {
    emojiMaps['aru-' + i] = emoji('aru', i, 'gif');
  }
  for (var i = 1; i <= 125; i++) {
    emojiMaps['twemoji-' + i] = emoji('twemoji', i, 'png');
  }
  for (var i = 1; i <= 4; i++) {
    emojiMaps['weibo-' + i] = emoji('weibo', i, 'png');
  }
  function pjax_valine() {
    if(!document.querySelectorAll("#valine_container")[0])return;
    let pagePlaceholder = pdata.commentPlaceholder || "快来评论吧~";
    let path = pdata.commentPath;
    if (path.length == 0) {
      let defaultPath = '';
      path = defaultPath || decodeURI(window.location.pathname);
    }
    var valine = new Valine();
    valine.init(Object.assign({"path":null,"placeholder":"快来评论吧~","appId":"rFjxhNkjfJEFwCxatct9lFP5-gzGzoHsz","appKey":"bPYEsXC56n2ziDYGE5pg7EHW","meta":["nick","mail"],"requiredFields":["nick"],"enableQQ":true,"recordIP":false,"avatar":"robohash","pageSize":10,"lang":"zh-cn","highlight":true,"mathJax":false,"js":"https://cdn.jsdelivr.net/npm/valine@1.4/dist/Valine.min.js"}, {
      el: '#valine_container',
      path: path,
      placeholder: pagePlaceholder,
      emojiCDN: 'https://cdn.jsdelivr.net/gh/volantis-x/cdn-emoji/valine/',
      emojiMaps: emojiMaps,
    }))
  }
  $(function () {
    pjax_valine();
  });
  volantis.pjax.push(pjax_valine);
</script>




  <script defer src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-busuanzi@2.3/js/busuanzi.pure.mini.js" data-pjax></script>



  
<script src="/android-note/js/app.js"></script>



<!-- optional -->

  <script>
const SearchServiceimagePath="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@master/img/";
const ROOT =  ("/android-note/" || "/").endsWith('/') ? ("/android-note/" || "/") : ("/android-note//" || "/" );

$('.input.u-search-input').one('focus',function(){
	
		loadScript('/android-note/js/search/hexo.js',setSearchService);
	
})

function listenSearch(){
  
    customSearch = new HexoSearch({
      imagePath: SearchServiceimagePath
    });
  
}
function setSearchService() {
	listenSearch();
	
}
</script>





  
<script src="https://cdn.jsdelivr.net/npm/node-waves@0.7.6/dist/waves.min.js"></script>

<script type="text/javascript">
$(function () {
  Waves.attach('.flat-btn', ['waves-button']);
  Waves.attach('.float-btn', ['waves-button', 'waves-float']);
  Waves.attach('.float-btn-light', ['waves-button', 'waves-float', 'waves-light']);
  Waves.attach('.flat-box', ['waves-block']);
  Waves.attach('.float-box', ['waves-block', 'waves-float']);
  Waves.attach('.waves-image');
  Waves.init();
});
</script>



  
<script src="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@2/js/comment_typing.js"></script>






  <script defer>

  const LCCounter = {
    app_id: 'rFjxhNkjfJEFwCxatct9lFP5-gzGzoHsz',
    app_key: 'bPYEsXC56n2ziDYGE5pg7EHW',
    custom_api_server: '',

    // 查询存储的记录
    getRecord(Counter, url, title) {
      return new Promise(function (resolve, reject) {
        Counter('get', '/classes/Counter?where=' + encodeURIComponent(JSON.stringify({url})))
          .then(resp => resp.json())
          .then(({results, code, error}) => {
            if (code === 401) {
              throw error;
            }
            if (results && results.length > 0) {
              var record = results[0];
              resolve(record);
            } else {
              Counter('post', '/classes/Counter', {url, title: title, times: 0})
                .then(resp => resp.json())
                .then((record, error) => {
                  if (error) {
                    throw error;
                  }
                  resolve(record);
                }).catch(error => {
                console.error('Failed to create', error);
                reject(error);
              });
            }
          }).catch((error) => {
          console.error('LeanCloud Counter Error:', error);
          reject(error);
        });
      })
    },

    // 发起自增请求
    increment(Counter, incrArr) {
      return new Promise(function (resolve, reject) {
        Counter('post', '/batch', {
          "requests": incrArr
        }).then((res) => {
          res = res.json();
          if (res.error) {
            throw res.error;
          }
          resolve(res);
        }).catch((error) => {
          console.error('Failed to save visitor count', error);
          reject(error);
        });
      });
    },

    // 构建自增请求体
    buildIncrement(objectId) {
      return {
        "method": "PUT",
        "path": `/1.1/classes/Counter/${ objectId }`,
        "body": {
          "times": {
            '__op': 'Increment',
            'amount': 1
          }
        }
      }
    },

    // 校验是否为有效的 UV
    validUV() {
      var key = 'LeanCloudUVTimestamp';
      var flag = localStorage.getItem(key);
      if (flag) {
        // 距离标记小于 24 小时则不计为 UV
        if (new Date().getTime() - parseInt(flag) <= 86400000) {
          return false;
        }
      }
      localStorage.setItem(key, new Date().getTime().toString());
      return true;
    },

    addCount(Counter) {
      var enableIncr = '' === 'true' && window.location.hostname !== 'localhost';
      enableIncr = true;
      var getterArr = [];
      var incrArr = [];
      // 请求 PV 并自增
      var pvCtn = document.querySelector('#lc-sv');
      if (pvCtn || enableIncr) {
        var pvGetter = this.getRecord(Counter, 'http://xander_wang.gitee.io/android-note' + '/#lc-sv', 'Visits').then((record) => {
          incrArr.push(this.buildIncrement(record.objectId))
          var eles = document.querySelectorAll('#lc-sv #number');
          if (eles.length > 0) {
            eles.forEach((el,index,array)=>{
              el.innerText = record.times + 1;
              if (pvCtn) {
                pvCtn.style.display = 'inline';
              }
            })
          }
        });
        getterArr.push(pvGetter);
      }

      // 请求 UV 并自增
      var uvCtn = document.querySelector('#lc-uv');
      if (uvCtn || enableIncr) {
        var uvGetter = this.getRecord(Counter, 'http://xander_wang.gitee.io/android-note' + '/#lc-uv', 'Visitors').then((record) => {
          var vuv = this.validUV();
          vuv && incrArr.push(this.buildIncrement(record.objectId))
          var eles = document.querySelectorAll('#lc-uv #number');
          if (eles.length > 0) {
            eles.forEach((el,index,array)=>{
              el.innerText = record.times + (vuv ? 1 : 0);
              if (uvCtn) {
                uvCtn.style.display = 'inline';
              }
            })
          }
        });
        getterArr.push(uvGetter);
      }

      // 请求文章的浏览数，如果是当前页面就自增
      var allPV = document.querySelectorAll('#lc-pv');
      if (allPV.length > 0 || enableIncr) {
        for (i = 0; i < allPV.length; i++) {
          let pv = allPV[i];
          let title = pv.getAttribute('data-title');
          var url = 'http://xander_wang.gitee.io/android-note' + pv.getAttribute('data-path');
          if (url) {
            var viewGetter = this.getRecord(Counter, url, title).then((record) => {
              // 是当前页面就自增
              let curPath = window.location.pathname;
              if (curPath.includes('index.html')) {
                curPath = curPath.substring(0, curPath.lastIndexOf('index.html'));
              }
              if (pv.getAttribute('data-path') == curPath) {
                incrArr.push(this.buildIncrement(record.objectId));
              }
              if (pv) {
                var ele = pv.querySelector('#lc-pv #number');
                if (ele) {
                  if (pv.getAttribute('data-path') == curPath) {
                    ele.innerText = (record.times || 0) + 1;
                  } else {
                    ele.innerText = record.times || 0;
                  }
                  pv.style.display = 'inline';
                }
              }
            });
            getterArr.push(viewGetter);
          }
        }
      }

      // 如果启动计数自增，批量发起自增请求
      if (enableIncr) {
        Promise.all(getterArr).then(() => {
          incrArr.length > 0 && this.increment(Counter, incrArr);
        })
      }

    },


    fetchData(api_server) {
      var Counter = (method, url, data) => {
        return fetch(`${ api_server }/1.1${ url }`, {
          method,
          headers: {
            'X-LC-Id': this.app_id,
            'X-LC-Key': this.app_key,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(data)
        });
      };
      this.addCount(Counter);
    },

    refreshCounter() {
      var api_server = this.app_id.slice(-9) !== '-MdYXbMMI' ? this.custom_api_server : `https://${ this.app_id.slice(0, 8).toLowerCase() }.api.lncldglobal.com`;
      if (api_server) {
        this.fetchData(api_server);
      } else {
        fetch('https://app-router.leancloud.cn/2/route?appId=' + this.app_id)
          .then(resp => resp.json())
          .then(({api_server}) => {
            this.fetchData('https://' + api_server);
          });
      }
    }

  };

  LCCounter.refreshCounter();

  document.addEventListener('pjax:complete', function () {
    LCCounter.refreshCounter();
  });
</script>




  <script>
const rootElement = document.documentElement;
const darkModeStorageKey = "user-color-scheme";
const rootElementDarkModeAttributeName = "data-user-color-scheme";

const setLS = (k, v) => {
    localStorage.setItem(k, v);
};

const removeLS = (k) => {
    localStorage.removeItem(k);
};

const getLS = (k) => {
    return localStorage.getItem(k);
};

const getModeFromCSSMediaQuery = () => {
  return window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light";
};

const resetRootDarkModeAttributeAndLS = () => {
  rootElement.removeAttribute(rootElementDarkModeAttributeName);
  removeLS(darkModeStorageKey);
};

const validColorModeKeys = {
  dark: true,
  light: true,
};

const applyCustomDarkModeSettings = (mode) => {
  const currentSetting = mode || getLS(darkModeStorageKey);

  if (currentSetting === getModeFromCSSMediaQuery()) {
    resetRootDarkModeAttributeAndLS();
  } else if (validColorModeKeys[currentSetting]) {
    rootElement.setAttribute(rootElementDarkModeAttributeName, currentSetting);
  } else {
    resetRootDarkModeAttributeAndLS();
  }
};

const invertDarkModeObj = {
  dark: "light",
  light: "dark",
};

/**
 * get target mode
 */
const toggleCustomDarkMode = () => {
  let currentSetting = getLS(darkModeStorageKey);

  if (validColorModeKeys[currentSetting]) {
    currentSetting = invertDarkModeObj[currentSetting];
  } else if (currentSetting === null) {
    currentSetting = invertDarkModeObj[getModeFromCSSMediaQuery()];
  } else {
    return;
  }
  setLS(darkModeStorageKey, currentSetting);
  return currentSetting;
};

/**
 * bind click event for toggle button
 */
var btn=$("#wrapper .toggle-mode-btn,#rightmenu-wrapper .toggle-mode-btn");
function bindToggleButton() {
    btn.on('click',(e) => {
      const mode = toggleCustomDarkMode();
      applyCustomDarkModeSettings(mode);
    });
}

applyCustomDarkModeSettings();
document.addEventListener("DOMContentLoaded", bindToggleButton);
volantis.pjax.push(bindToggleButton);
volantis.pjax.send(()=>{
	btn.unbind('click');
},'toggle-mode-btn-unbind');
</script>








<script>
function listennSidebarTOC() {
  const navItems = document.querySelectorAll(".toc li");
  if (!navItems.length) return;
  const sections = [...navItems].map((element) => {
    const link = element.querySelector(".toc-link");
    const target = document.getElementById(
      decodeURI(link.getAttribute("href")).replace("#", "")
    );
    link.addEventListener("click", (event) => {
      event.preventDefault();
      window.scrollTo({
		top: target.offsetTop + 100,
		
		behavior: "smooth"
		
	  });
    });
    return target;
  });

  function activateNavByIndex(target) {
    if (target.classList.contains("active-current")) return;

    document.querySelectorAll(".toc .active").forEach((element) => {
      element.classList.remove("active", "active-current");
    });
    target.classList.add("active", "active-current");
    let parent = target.parentNode;
    while (!parent.matches(".toc")) {
      if (parent.matches("li")) parent.classList.add("active");
      parent = parent.parentNode;
    }
  }

  function findIndex(entries) {
    let index = 0;
    let entry = entries[index];
    if (entry.boundingClientRect.top > 0) {
      index = sections.indexOf(entry.target);
      return index === 0 ? 0 : index - 1;
    }
    for (; index < entries.length; index++) {
      if (entries[index].boundingClientRect.top <= 0) {
        entry = entries[index];
      } else {
        return sections.indexOf(entry.target);
      }
    }
    return sections.indexOf(entry.target);
  }

  function createIntersectionObserver(marginTop) {
    marginTop = Math.floor(marginTop + 10000);
    let intersectionObserver = new IntersectionObserver(
      (entries, observe) => {
        let scrollHeight = document.documentElement.scrollHeight + 100;
        if (scrollHeight > marginTop) {
          observe.disconnect();
          createIntersectionObserver(scrollHeight);
          return;
        }
        let index = findIndex(entries);
        activateNavByIndex(navItems[index]);
      },
      {
        rootMargin: marginTop + "px 0px -100% 0px",
        threshold: 0,
      }
    );
    sections.forEach((element) => {
      element && intersectionObserver.observe(element);
    });
  }
  createIntersectionObserver(document.documentElement.scrollHeight);
}

document.addEventListener("DOMContentLoaded", listennSidebarTOC);
document.addEventListener("pjax:success", listennSidebarTOC);
</script>

<!-- more -->

 
	   
	    


<script src="https://cdn.jsdelivr.net/npm/pjax@0.2.8/pjax.min.js"></script>


<script>
    var pjax;
    document.addEventListener('DOMContentLoaded', function () {
      pjax = new Pjax({
        elements: 'a[href]:not([href^="#"]):not([href="javascript:void(0)"]):not([pjax-fancybox])',
        selectors: [
          "title",
          
          "#pjax-container",
          "#pjax-header-nav-list"
        ],
        cacheBust: false,   // url 地址追加时间戳，用以避免浏览器缓存
        timeout: 5000
      });
    });

    document.addEventListener('pjax:send', function (e) {
      //window.stop(); // 相当于点击了浏览器的停止按钮

      try {
        var currentUrl = window.location.pathname;
        var targetUrl = e.triggerElement.href;
        var banUrl = [""];
        if (banUrl[0] != "") {
          banUrl.forEach(item => {
            if(currentUrl.indexOf(item) != -1 || targetUrl.indexOf(item) != -1) {
              window.location.href = targetUrl;
            }
          });
        }
      } catch (error) {}

      window.subData = null; // 移除标题（用于一二级导航栏切换处）

      volantis.$switcher.removeClass('active'); // 关闭移动端激活的搜索框
      volantis.$header.removeClass('z_search-open'); // 关闭移动端激活的搜索框
      volantis.$wrapper.removeClass('sub'); // 跳转页面时关闭二级导航

      // 解绑事件 避免重复监听
      volantis.$topBtn.unbind('click');
      $('.menu a').unbind('click');
      $(window).unbind('resize');
      $(window).unbind('scroll');
      $(document).unbind('scroll');
      $(document).unbind('click');
      $('body').unbind('click');
	  // 使用 volantis.pjax.send 方法传入pjax:send回调函数 参见layout/_partial/scripts/global.ejs
	  volantis.pjax.method.send.start();
    });

    document.addEventListener('pjax:complete', function () {
      $('.nav-main').find('.list-v').not('.menu-phone').removeAttr("style",""); // 移除小尾巴的移除
      $('.menu-phone.list-v').removeAttr("style",""); // 移除小尾巴的移除
      $('script[data-pjax], .pjax-reload script').each(function () {
        $(this).parent().append($(this).remove());
      });
      try{
		// 使用 volantis.pjax.push 方法传入重载函数 参见layout/_partial/scripts/global.ejs
		volantis.pjax.method.complete.start();
      } catch (e) {
        console.log(e);
      }
    });

    document.addEventListener('pjax:error', function (e) {
	  // 使用 volantis.pjax.error 方法传入pjax:error回调函数 参见layout/_partial/scripts/global.ejs
	  volantis.pjax.method.error.start();
      window.location.href = e.triggerElement.href;
    });
</script>
 
	  
    </div>
  </body>
</html>
